/*
Copyright (c) 2009 Yahoo! Inc.  All rights reserved.  
The copyrights embodied in the content of this file are licensed under the BSD (revised) open source license
*/
package com.yahoo.astra.fl.charts.axes
{
	import com.yahoo.astra.fl.charts.series.ISeries;
	import com.yahoo.astra.fl.utils.UIComponentUtil;
	import com.yahoo.astra.utils.NumberUtil;
	import com.yahoo.astra.fl.charts.CartesianChart;	
	
	import flash.utils.Dictionary;
	import fl.core.UIComponent;
	
	/**
	 * An axis type representing a numeric range from minimum to maximum
	 * with major and minor divisions.
	 * 
	 * @author Josh Tynjala
	 */
	public class NumericAxis extends BaseAxis implements IAxis, IOriginAxis, IStackingAxis
	{
		
	//--------------------------------------
	//  Constructor
	//--------------------------------------
	
		/**
		 * Constructor.
		 */
		public function NumericAxis()
		{
		}

	//--------------------------------------
	//  Properties
	//--------------------------------------
		
		/**
		 * @private
		 * The multiplier used to calculate the position on the renderer from an
		 * axis value.
		 */
		protected var positionMultiplier:Number = 0;
		
		/**
		 * @private
		 * Storage for the minimum value.
		 */
		private var _minimum:Number = 0;
		
		/**
		 * @private
		 * Indicates whether the minimum bound is user-defined or generated by the axis.
		 */
		private var _minimumSetByUser:Boolean = false;
		
		/**
		 * The minimum value displayed on the axis. By default, this value is generated
		 * by the axis itself. If the user defines this value, the axis will skip this
		 * automatic generation. To enable this behavior again, set this property to NaN.
		 */
		public function get minimum():Number
		{
			return this._minimum;
		}
		
		/**
		 * @private
		 */
		public function set minimum(value:Number):void
		{
			this._minimum = value;
			this._minimumSetByUser = !isNaN(value);
		}
	
		/**
		 * @private
		 * Storage for the maximum value.
		 */
		private var _maximum:Number = 100;
		
		/**
		 * @private
		 * Indicates whether the maximum bound is user-defined or generated by the axis.
		 */
		private var _maximumSetByUser:Boolean = false;
		
		/**
		 * The maximum value displayed on the axis. By default, this value is generated
		 * by the axis itself. If the user defines this value, the axis will skip this
		 * automatic generation. To enable this behavior again, set this property to NaN.
		 */
		public function get maximum():Number
		{
			return this._maximum;
		}
		
		/**
		 * @private
		 */
		public function set maximum(value:Number):void
		{
			this._maximum = value;
			this._maximumSetByUser = !isNaN(value);
		}
		
	//-- Units
	
		/**
		 * @private
		 * Storage for the major unit.
		 */
		private var _majorUnit:Number = 10;
		
		/**
		 * @private
		 * Indicates whether the major unit is user-defined or generated by the axis.
		 */
		private var _majorUnitSetByUser:Boolean = false;
		
		/**
		 * The major unit at which new ticks and labels are drawn. By default, this value
		 * is generated by the axis itself. If the user defines this value, the axis will
		 * skip the automatic generation. To enable this behavior again, set this property
		 * to NaN.
		 */
		public function get majorUnit():Number
		{
			return this._majorUnit;
		}
		
		/**
		 * @private
		 */
		public function set majorUnit(value:Number):void
		{
			this._majorUnit = value;
			this._majorUnitSetByUser = !isNaN(value);
		}
	
		/**
		 * @private
		 * Storage for the minor unit.
		 */
		private var _minorUnit:Number = 0;
		
		/**
		 * @private
		 * Indicates whether the minor unit is user-defined or generated by the axis.
		 */
		private var _minorUnitSetByUser:Boolean = false;
		
		/**
		 * The minor unit at which new ticks are drawn. By default, this value
		 * is generated by the axis itself. If the user defines this value, the axis will
		 * skip the automatic generation. To enable this behavior again, set this property
		 * to NaN.
		 */
		public function get minorUnit():Number
		{
			return this._minorUnit;
		}
		
		/**
		 * @private
		 */
		public function set minorUnit(value:Number):void
		{
			this._minorUnit = value;
			this._minorUnitSetByUser = !isNaN(value);
		}
		
		/**
		 * @inheritDoc
		 */
		public function get origin():Object
		{
			var origin:Number = 0;
			if(this.scale == ScaleType.LOGARITHMIC)
			{
				origin = 1;
			}
			
			origin = Math.max(origin, this.minimum);
			origin = Math.min(origin, this.maximum);
			return origin;
		}
		
		/**
		 * @private
		 * Storage for the stackingEnabled property.
		 */
		private var _stackingEnabled:Boolean = false;
		
		/**
		 * @inheritDoc
		 */
		public function get stackingEnabled():Boolean
		{
			return this._stackingEnabled;
		}
		
		/**
		 * @private
		 */
		public function set stackingEnabled(value:Boolean):void
		{
			this._stackingEnabled = value;
		}
	
		/**
		 * @private
		 * Storage for the alwaysShowZero property.
		 */
		private var _alwaysShowZero:Boolean = true;
		
		/**
		 * If true, the axis will attempt to keep zero visible at all times.
		 * If both the minimum and maximum values displayed on the axis are
		 * above zero, the minimum will be reset to zero. If both minimum and
		 * maximum appear below zero, the maximum will be reset to zero. If
		 * the minimum and maximum appear at positive and negative values
		 * respectively, zero is already visible and the axis scale does not
		 * change.
		 * 
		 * <p>This property has no affect if you manually set the minimum and
		 * maximum values of the axis.</p>
		 */
		public function get alwaysShowZero():Boolean
		{
			return this._alwaysShowZero;
		}
		
		/**
		 * @private
		 */
		public function set alwaysShowZero(value:Boolean):void
		{
			this._alwaysShowZero = value;
		}
	
		/**
		 * @private
		 * Storage for the snapToUnits property.
		 */
		private var _snapToUnits:Boolean = true;
		
		/**
		 * If true, the labels, ticks, gridlines, and other objects will snap to
		 * the nearest major or minor unit. If false, their position will be based
		 * on the minimum value.
		 */
		public function get snapToUnits():Boolean
		{
			return this._snapToUnits;
		}
		
		/**
		 * @private
		 */
		public function set snapToUnits(value:Boolean):void
		{
			this._snapToUnits = value;
		}
		
		/**
		 * @private
		 * Storage for the scale property.
		 */
		private var _scale:String = ScaleType.LINEAR;
		
		/**
		 * The type of scaling used to display items on the axis.
		 * 
		 * @see com.yahoo.astra.fl.charts.ScaleType
		 */
		public function get scale():String
		{
			return this._scale;
		}
		
		/**
		 * @private
		 */
		public function set scale(value:String):void
		{
			this._scale = value;
		}
		
		/**
		 * @private
		 */
		private var _dataMinimum:Number = NaN;
		
		/**
		 * @private
		 */
		private var _dataMaximum:Number = NaN;
		
		/**
		 * @private
		 */
		private var _numLabels:Number;
		
		/**
		 * @private
		 */		
		private var _numLabelsSetByUser:Boolean = false;

		/**
		 * @inheritDoc
		 */
		public function get numLabels():Number
		{
			return _numLabels;
		}
		
		/**
		 * @private (setter)
		 */
		public function set numLabels(value:Number):void
		{
			if(_numLabelsSetByUser) return;
			_numLabels = value;
			_numLabelsSetByUser = true;
			_majorUnitSetByUser = false;
			_minorUnitSetByUser = false;
		}		
			
		/**
		 * @private 
		 */
		private var _roundMajorUnit:Boolean = true;
		
		/**
		 * Indicates whether to round the major unit
		 */
		public function get roundMajorUnit():Boolean
		{
			return _roundMajorUnit;
		}
		
		/**
		 * @private (setter)
		 */
		public function set roundMajorUnit(value:Boolean):void
		{
			_roundMajorUnit = value;
		}
		
		/**
		 * @private
		 * Holds value for idealPixels
		 */
		private var _idealPixels:Number = 70;
		
		/**
		 * Desired distance between majorUnits. Used to calculate the major unit
		 * when unspecified and <code>calculateByLabelSize</code> is set to false.
		 */
		public function get idealPixels():Number
		{
			return _idealPixels;
		}
		
		/**
		 * @private (setter)
		 */
		public function set idealPixels(value:Number):void
		{
			_idealPixels = value;
		}
		
		/**
		 * @private
		 * Holds value for calculateByLabelSize
		 */
		private var _calculateByLabelSize:Boolean = false;
		
		/** 
		 * Indicates whether to use the maximum size of an axis label 
		 * when calculating the majorUnit.
		 */
		public function get calculateByLabelSize():Boolean
		{
			return _calculateByLabelSize;
		}
		
		/** 
		 * @private (setter)
		 */
		public function set calculateByLabelSize(value:Boolean):void
		{
			_calculateByLabelSize = value;
		}
		
	//--------------------------------------
	//  Public Methods
	//--------------------------------------
	
		/**
		 * @inheritDoc
		 */
		public function valueToLocal(data:Object):Number
		{
			if(data == null)
			{
				//bad data. a properly-designed renderer will not draw this.
				return NaN;
			}
			
			var position:Number = 0;
			
			if(this.scale == ScaleType.LINEAR)
			{
				position = (Number(data) - this.minimum) * this.positionMultiplier;
			}
			else
			{
				var logOfData:Number = Math.log(Number(data));
				var logOfMinimum:Number = Math.log(this.minimum);
				position = (logOfData - logOfMinimum) * this.positionMultiplier;
			}
			
			if(this.reverse)
			{
				position = this.renderer.length - position;
			}
			
			//the vertical axis has its origin on the bottom
			if(this.renderer is ICartesianAxisRenderer && ICartesianAxisRenderer(this.renderer).orientation == AxisOrientation.VERTICAL)
			{
				position = this.renderer.length - position;
			}
			
			return Math.round(position);
		}
	
		/**
		 * @inheritDoc
		 */
		public function stack(top:Object, ...rest:Array):Object
		{
			var numericValue:Number = Number(top);
			var negative:Boolean = false;
			if(numericValue < 0)
			{
				negative = true;
			}
			
			var restCount:int = rest.length;
			for(var i:int = 0; i < restCount; i++)
			{
				var currentValue:Number = Number(rest[i]);
				if(negative && currentValue < 0)
				{
					numericValue += currentValue;
				}
				else if(!negative && currentValue > 0)
				{
					numericValue += currentValue;
				}
			}
			return numericValue;
		}
			
		/**
		 * @inheritDoc
		 */
		public function updateScale():void
		{			
			this.resetScale();
			this.calculatePositionMultiplier();
			
			(this.renderer as ICartesianAxisRenderer).majorUnitSetByUser = this._majorUnitSetByUser;
			this.renderer.ticks = this.createAxisData(this.majorUnit);
			this.renderer.minorTicks = this.createAxisData(this.minorUnit);
		}

		/**
		 * @inheritDoc
		 */
		public function getMaxLabel():String
		{
			var difference:Number = Math.round(this.maximum - this.minimum);
			var maxString:String = this.valueToLabel(this.maximum);
			var minString:String = this.valueToLabel(this.minimum);
			var halfString:String = this.valueToLabel(Math.round(difference/2));
			var thirdString:String = this.valueToLabel(Math.round(difference/3));
			if(maxString.length < minString.length) maxString = minString;
			if(halfString.length > maxString.length) maxString = halfString;
			if(thirdString.length > maxString.length) maxString = thirdString;
			return maxString as String;	
		}
		
	//--------------------------------------
	//  Protected Methods
	//--------------------------------------
	
		/**
		 * @private
		 * If the minimum, maximum, major unit or minor unit have not been set by the user,
		 * these values must be generated by the axis. May be overridden to use custom
		 * scaling algorithms.
		 */
		protected function resetScale():void
		{	
			//use the discovered min and max from the data
			//if the developer didn't specify anything
			if(!this._minimumSetByUser)
			{
				this._minimum = this._dataMinimum;
			}
			if(!this._maximumSetByUser)
			{
				this._maximum = this._dataMaximum;
			}
			
			this.checkMinLessThanMax();
			
			this.pinToOrigin();
			
			this.calculateMajorUnit();
			this.adjustMinAndMaxFromMajorUnit();
				
			this.correctLogScaleMinimum();
			
			//ensure that min != max
			if(!this._maximumSetByUser && this._minimum == this._maximum)
			{
				this._maximum = this._minimum + 1;
				if(!this._majorUnitSetByUser)
				{
					//rarely happens, so I'll hardcode a nice major unit
					//for our difference of one
					this._majorUnit = 0.5;
				}
			}
			
			this.calculateMinorUnit();
			
			//even if they are manually set by the user, check all values for possible floating point errors.
			//we don't want extra labels or anything like that!
			this._minimum = NumberUtil.roundToPrecision(this._minimum, 10);
			this._maximum = NumberUtil.roundToPrecision(this._maximum, 10);
			this._majorUnit = NumberUtil.roundToPrecision(this._majorUnit, 10);
			this._minorUnit = NumberUtil.roundToPrecision(this._minorUnit, 10);
		}

		/**
		 * @private
		 * Determines the best major unit.
		 */
		protected function calculateMajorUnit():void
		{
			if(this._majorUnitSetByUser)
			{
				return;
			}
			
			var chart:CartesianChart = this.chart as CartesianChart;
			var labelSpacing:Number = 0;
			var approxLabelDistance:Number = this.idealPixels;
			var overflow:Number = 0;
			if(this.calculateByLabelSize)
			{
				var rotation:Number;
				//Check to see if this axis is horizontal. Since the width of labels will be variable, we will need to apply a different alogrithm to determine the majorUnit.
				if(chart.horizontalAxis == this)
				{
					//extract the approximate width of the labels by getting the textWidth of the maximum date when rendered by the label function with the textFormat of the renderer.
					approxLabelDistance = this.maxLabelWidth;
					rotation = chart.getHorizontalAxisStyle("rotation") as Number;
					if(rotation >= 0)
					{
						if(!isNaN(chart.horizontalAxisLabelData.rightLabelOffset)) overflow += chart.horizontalAxisLabelData.rightLabelOffset as Number;
					}
					if(rotation <= 0)
					{
						if(!isNaN(chart.horizontalAxisLabelData.leftLabelOffset)) overflow += chart.horizontalAxisLabelData.leftLabelOffset as Number;
					}								
				}
				else
				{
					approxLabelDistance = this.maxLabelHeight;	
					rotation = chart.getVerticalAxisStyle("rotation") as Number;
					if(!isNaN(chart.verticalAxisLabelData.topLabelOffset)) overflow = chart.verticalAxisLabelData.topLabelOffset as Number;					
				}
				labelSpacing = this.labelSpacing; 
				approxLabelDistance += (labelSpacing*2);
			}
			
			var difference:Number = this.maximum - this.minimum;
			var tempMajorUnit:Number = 0; 

			var maxLabels:Number = ((this.renderer.length + overflow) - labelSpacing)/approxLabelDistance;
			
			if(this.calculateByLabelSize) 
			{
				maxLabels = Math.floor(maxLabels);
				//Adjust the max labels to account for potential maximum and minimum adjustments that may occur.
				if(!this._maximumSetByUser && !this._minimumSetByUser && !(this.alwaysShowZero && this._minimum == 0)) maxLabels -= 1;
			}
			
			//If set by user, use specified number of labels unless its too many
			if(this._numLabelsSetByUser)
			{
				maxLabels = Math.min(maxLabels, this.numLabels);
			}

			tempMajorUnit = difference/maxLabels;
			
			if(!this.calculateByLabelSize)
			{
				tempMajorUnit = this.niceNumber(tempMajorUnit);			
			}
			else if(this.roundMajorUnit)
			{
				var order:Number = Math.ceil(Math.log(tempMajorUnit) * Math.LOG10E);
				var roundedMajorUnit:Number = Math.pow(10, order);

				if (roundedMajorUnit / 2 >= tempMajorUnit) 
				{
					var roundedDiff:Number = Math.floor((roundedMajorUnit / 2 - tempMajorUnit) / (Math.pow(10,order-1)/2));
				 	tempMajorUnit = roundedMajorUnit/2 - roundedDiff*Math.pow(10,order-1)/2;
				}
				else 
				{
					tempMajorUnit = roundedMajorUnit;
				}
			}
			
			if(!isNaN(tempMajorUnit)) this._majorUnit = tempMajorUnit;										
		}

		/**
		 * @private
		 * Determines the best minor unit.
		 */
		protected function calculateMinorUnit():void
		{
			if(this._minorUnitSetByUser)
			{
				return;
			}
			
			var range:Number = this.maximum - this.minimum;
			var majorUnitSpacing:Number = this.renderer.length * (this.majorUnit / range);

			if(this._majorUnit != 1)
			{
				if(this._majorUnit % 2 == 0)
				{
					this._minorUnit = this._majorUnit / 2;
				}
				else if(this._majorUnit % 3 == 0)
				{
					this._minorUnit = this._majorUnit / 3;
				}
				else this._minorUnit = 0;
			}
		}
		
		/**
		 * @private
		 * Creates the AxisData objects for the axis renderer.
		 */
		protected function createAxisData(unit:Number):Array
		{
			if(unit <= 0)
			{
				return [];
			}
			
			var data:Array = [];
			var displayedMaximum:Boolean = false;
			var value:Number = this.minimum;
			while(value < this.maximum || NumberUtil.fuzzyEquals(value, this.maximum))
			{
				if(value % 1 != 0) value = NumberUtil.roundToPrecision(value, 10);
				
				//because Flash UIComponents round the position to the nearest pixel, we need to do the same.
				var position:Number = Math.round(this.valueToLocal(value));
				var label:String = this.valueToLabel(value);
				var axisData:AxisData = new AxisData(position, value, label);
				data.push(axisData);
				
				//if the maximum has been displayed, we're done!
				if(displayedMaximum) break;
				
				//a bad unit will get us stuck in an infinite loop
				if(unit <= 0)
				{
					value = this.maximum;
				}
				else
				{
					value += unit;
					if(this.snapToUnits && !this._minimumSetByUser && this.alwaysShowZero)
					{
						value = NumberUtil.roundDownToNearest(value, unit);
					}
					if(this._majorUnitSetByUser) value = Math.min(value, this.maximum);
				}
				displayedMaximum = NumberUtil.fuzzyEquals(value, this.maximum);
			}
			return data;
		}
		
	//--------------------------------------
	//  Private Methods
	//--------------------------------------
	
		/**
		 * @private
		 * If we want to always show zero, corrects the min or max as needed.
		 */
		private function pinToOrigin():void
		{
			//if we're pinned to zero, and min or max is supposed to be generated,
			//make sure zero is somewhere in the range
			if(this.alwaysShowZero)
			{
				if(!this._minimumSetByUser && this._minimum > 0 && this._maximum > 0)
				{
					this._minimum = 0;
				}
				else if(!this._maximumSetByUser && this._minimum < 0 && this._maximum < 0)
				{
					this._maximum = 0;
				}
			}
		}
		
		/**
		 * @private
		 * Increases the maximum and decreases the minimum based on the major unit.
		 */
		private function adjustMinAndMaxFromMajorUnit():void
		{
			//adjust the maximum so that it appears on a major unit
			//but don't change the maximum if the user set it or it is pinned to zero
			if(!this._maximumSetByUser && !(this.alwaysShowZero && this._maximum == 0))
			{
				var oldMaximum:Number = this._maximum;
				if(this._minimumSetByUser)
				{	
					//if the user sets the minimum, we need to ensure that the maximum is an increment of the major unit starting from 
					//the minimum instead of zero
					this._maximum = NumberUtil.roundUpToNearest(this._maximum - this._minimum, this._majorUnit);
					this._maximum += this._minimum;
				}
				else
				{
					this._maximum = NumberUtil.roundUpToNearest(this._maximum, this._majorUnit);
				}
				
				//uncomment to include an additional major unit in this adjustment
				if(this._maximum == oldMaximum /*|| this._maximum - oldMaximum < this._majorUnit */)
				{
					this._maximum += this._majorUnit;
				}
			}
			
			//adjust the minimum so that it appears on a major unit
			//but don't change the minimum if the user set it or it is pinned to zero
			if(!this._minimumSetByUser && !(this.alwaysShowZero && this._minimum == 0))
			{
				var oldMinimum:Number = this._minimum;
				this._minimum = NumberUtil.roundDownToNearest(this._minimum, this._majorUnit);
				
				//uncomment to include an additional major unit in this adjustment
				if(this._minimum == oldMinimum /*|| oldMinimum - this._minimum < this._majorUnit*/)
				{
					this._minimum -= this._majorUnit;
				}
			}
		}
		
		/**
		 * @private
		 * If we're using logarithmic scale, corrects the minimum if it gets set
		 * to a value <= 0.
		 */
		private function correctLogScaleMinimum():void
		{
			//logarithmic scale can't have a minimum value <= 0. If that's the case, push it up to 1.0
			//TODO: Determine if there's a better way to handle this...
			if(!this._minimumSetByUser && this.scale == ScaleType.LOGARITHMIC && this._minimum <= 0)
			{
				//use the dataMinimum if it's between 0 and 1
				//otherwise, just use 1
				if(this._dataMinimum > 0 && this._dataMinimum < 1)
				{
					this._minimum = this._dataMinimum;
				}
				else
				{
					this._minimum = 1;
				}
			}
		}

		/**
		 * @private
		 * Calculates a "nice" number for use with major or minor units
		 * on the axis. Only returns numbers similar to 10, 20, 25, and 50.
		 */
		private function niceNumber(value:Number):Number
		{
			if(value == 0)
			{
				return 0;
			}
			
			var count:int = 0;
			while(value > 10.0e-8)
			{
				value /= 10;
				count++;
			}

			//all that division in the while loop up there
			//could cause rounding errors. Don't you hate that?
			value = NumberUtil.roundToPrecision(value, 10);

		    if(value > 4.0e-8)
		    {
		        value = 5.0e-8;
		    }
		    else if(value > 2.0e-8)
		    {
		        value = 2.5e-8;
		    }
		    else if(value > 1.0e-8)
		    {
		        value = 2.0e-8;
		    }
		    else
		    {
		        value = 1.0e-8;
		    }
					
		    for(var i:int = count; i > 0; i--)
		    {
		    	value *= 10;
		    }
		
		    return value;
		}
		
		/**
		 * @private
		 * Swaps the minimum and maximum values, if needed.
		 */
		private function checkMinLessThanMax():void
		{
			if(this._minimum > this._maximum)
			{
				var temp:Number = this._minimum;
				this._minimum = this._maximum;
				this._maximum = temp;
				
				//be sure to swap these flags too!
				var temp2:Boolean = this._minimumSetByUser;
				this._minimumSetByUser = this._maximumSetByUser;
				this._maximumSetByUser = temp2;
			}
		}
	
		/**
		 * @private
		 * Calculates the multiplier used to convert a data point to an actual position
		 * on the axis.
		 */
		private function calculatePositionMultiplier():void
		{
			var range:Number = this.maximum - this.minimum;
			if(this.scale == ScaleType.LOGARITHMIC)
			{
				range = Math.log(this.maximum) - Math.log(this.minimum);
			}
			
			if(range == 0)
			{
				this.positionMultiplier = 0;
				return;
			}			
			this.positionMultiplier = this.renderer.length / range;
		}
		/**
		 * @private
		 */
		override protected function parseDataProvider():void
		{
			var seriesCount:int = this.dataProvider.length;
			var dataMinimum:Number = NaN;
			var dataMaximum:Number = NaN;
			for(var i:int = 0; i < seriesCount; i++)
			{
				var series:ISeries = this.dataProvider[i] as ISeries;
				var seriesLength:int = series.length;
				for(var j:int = 0; j < seriesLength; j++)
				{
					var item:Object = series.dataProvider[j];
					if(item === null)
					{
						continue;
					}
					
					//automatically calculates stacked values
					var value:Number = Number(this.chart.itemToAxisValue(series, j, this));
					if(isNaN(value))
					{
						continue; //skip bad data
					}
					
					//don't let bad data propogate
					//Math.min()/Math.max() with a NaN argument will choose NaN. Ya Rly.
					dataMinimum = isNaN(dataMinimum) ? value : Math.min(dataMinimum, value);
					dataMaximum = isNaN(dataMaximum) ? value : Math.max(dataMaximum, value);
				}
			}
			
			if(!isNaN(dataMinimum) && !isNaN(dataMaximum))
			{
				this._dataMinimum = dataMinimum;
				this._dataMaximum = dataMaximum;
			}
			else
			{
				//some sensible defaults
				this._dataMinimum = 0;
				this._dataMaximum = 1;
			}
			
			if(!this._minimumSetByUser)
			{
				this._minimum = this._dataMinimum;
			}
			if(!this._maximumSetByUser)
			{
				this._maximum = this._dataMaximum;
			}												
		}		
	}
}